<?php

/**
 * Psop
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.opensource.org/licenses/bsd-license.php
 *
 * @category   Psan
 * @package    Psan_Psan
 * @copyright  Copyright (c) 2008 MaakSite.nl Productions. (http://productions.maaksite.nl/)
 * @license    http://www.opensource.org/licenses/bsd-license.php     New BSD License
 * @version    $Id: Psan.php 5 2008-04-01 18:27:58Z rutger@maaksite.nl $
 */

/**
 * @see Zend_Cache
 */
require_once 'Zend/Cache.php';
/**
 * @see Zend_Http_Client
 */
require_once 'Zend/Http/Client.php';
/**
 * @see Psan_Analyzer_Abstract
 */
require_once 'Psan/Analyzer/Abstract.php';
/**
 * @see Psan_Results
 */
require_once 'Psan/Results.php';

/**
 * @category   Psan
 * @package    Psan_Psan
 * @copyright  Copyright (c) 2008 MaakSite.nl Productions. (http://productions.maaksite.nl/)
 * @license    http://www.opensource.org/licenses/bsd-license.php     New BSD License
 */
class Psan_Psan
{
    /**
     * Http client
     *
     * @see Zend_Http_Client
     */
    private $_http;
    
    private $_cookies = array();
    
    private $config = array();
    
    private $baseUrl;
    
    private $_requests = 0;
    
    private $_pages = array();
    
    /**
     * Psan
     *
     * @param   string  $url    Location of website
     * @param   array   $config Configuration options
     * @return  void
     */
    public function __construct($url, $config = array())
    {
        // init uri parser
        $uri = Zend_Uri::factory($url);
        
        // config must be an array
        if (!is_array($config)) {
            throw new Zend_Exception('Config must be an array');
        }
        
        // check config
        if (!isset($config['analyzers']) || !is_array($config['analyzers'])) {
            /**
             * @see Psan_Psan_Analyzer_Html
             */
            require_once 'Psan/Psan/Analyzer/Html.php';
            
            $html = new Psan_Psan_Analyzer_Html();
            
            $config['analyzers'] = array(
            'text/html'         => $html,
            );
        }
        
        // cache dir
        $this->_cacheDir = ZEND_ROOT . '/cache/' . str_replace('/', '_', $url);

        if (is_dir($this->_cacheDir)) {
            $this->_cacheDir = ZEND_ROOT . '/cache/' . str_replace('/', '_', $url) . time();
        }
        $mkdir = @mkdir($this->_cacheDir);
        if (!$mkdir) {
            throw new Zend_Exception('Could not make dir \'' . $this->_cacheDir . '\'');
        }
        
        $this->_fopen = array();
        $this->_fopen['errors'] = fopen($this->_cacheDir . '/errors', 'a+');
        $this->_fopen['emails'] = fopen($this->_cacheDir . '/emails', 'a+');
        
        // set config
        $this->_config = $config;
        
        $path = $uri->getPath();
        if (substr($path, strlen($path)-1, 1) == '/') {
            $path = substr($path, 0, strlen($path)-1);
        }
        $this->_baseUrl = $uri->getScheme() . '://' . $uri->getHost() . '' . $path;
        
        $port = ($uri->getPort() == false) ? 80 : $uri->getPort();
        
        $this->_http = new Zend_Http_Client($url, array(
        'useragent'     => 'Psan/1.0 (+http://code.google.com/p/psan/)',
        'keepalive'     => true,
        //'maxredirects'  => 0,
        ));
        
        $this->_log('Scanning url "' . $uri . '" at ip ' . gethostbyname($uri->getHost()));

        $this->_crawl($url);
        
        echo "\n\n";
        echo $this->_requests . ' requests';
        echo "\n";
        
        echo 'Generating reports...' . "\n";
        
        $results = new Psan_Psan_Results($this->_fopen, $url);
    }
    
    private function _crawl ($url, $method = 'GET', $params = array(), $post = array()) {
        
        $this->_http->resetParameters();
        
        $this->_http->setParameterGet($params);
        $this->_http->setParameterPost($post);
        
        $method = strtoupper($method);
        
        if ($method == 'GET') {
            $this->_http->setEncType(Zend_Http_Client::ENC_URLENCODED);
        } else {
            $this->_http->setEncType(Zend_Http_Client::ENC_FORMDATA);
        }
        
        if ($method == 'GET') {
            $identifier = $url . '?' . http_build_query($params);
        } else {
            $identifier = $url . '?' . http_build_query($params) . '?' . http_build_query($post);
        }
        
        if (in_array($identifier, $this->_pages)) {
            ob_flush();
            return false;
        }
        
        $this->_pages[] = $identifier;
        
        try {
            $this->_http->setUri($url);
        } catch (Zend_Exception $e) {
            echo '!';
            return false;
        }
        
        // timeout
        #usleep(50);
        
        try {
            /**
             * @see Zend_Http_Response
             */
            $response = $this->_http->request($method);
        } catch (Zend_Exception $e) {
            $this->_cache('errors', array($identifier, $e->getMessage()));
            echo '!';
            ob_flush();
            return false;
        }
        
        // only crawl when status OK
        if ($response->getStatus() == 200) {
            $type = strtolower($response->getHeader('Content-type'));
            if (strpos($type, ';') != false) {
                $type = explode(';', $type);
                $type = $type[0];
            }
            
            if (isset($this->_config['analyzers'][$type])) {
                $this->_log('Crawling url "' . $url . '" ');
                
                echo '.';
                ob_flush();
                $this->_requests++;

                $this->_analyze($this->_config['analyzers'][$type]->parse($response->getBody(), $this->_baseUrl, $url));
            } else {
                $this->_log('Not crawling url "' . $url . '" (' . $type . ')');
                $this->_cache('errors', array($identifier, 'Mimetype invalid (' . $type . ')'));
                echo '!';
                ob_flush();
            }
        } else {
            $this->_log('Not crawling url "' . $url . '" (' . $response->getStatus() . ')');
            $this->_cache('errors', array($identifier, 'Status not OK (' . $response->getStatus() . ')', $post, $params, $url));
            echo '!';
            ob_flush();
        }
    }
    
    private function _analyze($links)
    {
        foreach ($links as $link) {
            switch ($link['type']) {
                case 'email':
                    $this->_cache('emails', $link['value']);
                    echo '#';
                    ob_flush();
                    break;
                case 'url':
                    $this->_crawl($link['value'], $link['method'], $link['params'], $link['post']);
                    break;
            }
        }
    }
    
    private function _log($content)
    {
        //echo $content . "\n";
        ob_flush();
    }
    
    private function _cache ($type, $values) {
        if (is_array($values)) {
            $value = serialize($values);
        } else {
            $value = $values;
        }
        fputs($this->_fopen[$type], $value . "\n");
    }
}
